Skip to content

ADFA-4331: Decode opened plugin tabs off the main thread#1416

Merged
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-4331-restoreplugintabs-json-main-thread
Jun 25, 2026
Merged

ADFA-4331: Decode opened plugin tabs off the main thread#1416
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-4331-restoreplugintabs-json-main-thread

Conversation

@fryanpan

@fryanpan fryanpan commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Jira Ticket: https://appdevforall.atlassian.net/browse/ADFA-4331
Sentry Issue: https://appdevforall-inc-9p.sentry.io/issues/APPDEVFORALL-1J

Question

Looks like this file is part of AndroidIDE -- do we make changes here or upstream?

Reproduction Details

A main-thread / ANR-risk performance issue (not a crash). EditorHandlerActivity.restoreOpenedPluginTabs() read the cached-tabs JSON from SharedPreferences and ran Gson().fromJson(...) synchronously on the main thread during startup, stalling the UI.

Stack Trace

No exception (profiling issue). Suspect function:

Gson.fromJson(JsonReader, TypeToken): Object   — on the main thread (warm start)

(issue type: profile_json_decode_main_thread · device.class: low)

User Steps

User steps leading up to crash, based on Sentry breadcrumbs:

  • App warm-start with previously-open plugin tabs to restore; the tab-state JSON is decoded on the main thread during onStart, blocking the first frames. (Profiling aggregate; tagged to the startup transaction.)

Was able to reproduce in a unit test?

Yes.
RestorePluginTabsThreadTest (:app, Robolectric) pins Dispatchers.Main, invokes the real private restoreOpenedPluginTabs(), and captures the thread on which its pref write executes (via the EventBus PreferenceChangeEvent). Baseline: write thread == main (FAIL); branch: write thread == DefaultDispatcher-worker (off main). Mutation-sensitive: flipping the prod file flips red/green.

What Was Fixed

Move the pref read + Gson decode + clearing write into withContext(Dispatchers.IO/Default) inside lifecycleScope.launch; UI updates stay on Main.

Testing

:app:testV8DebugUnitTest --tests …RestorePluginTabsThreadTest → green (red on baseline). Local CodeRabbit review: no findings. Reviewer notes (local): remove the leftover println("PROBE: …") debug line in the test; the restore now races the sibling onStart reopen-files coroutine (serialized on Main, theoretical only).


Fixes APPDEVFORALL-1J

@fryanpan fryanpan marked this pull request as ready for review June 19, 2026 11:15
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7093df1-0fb1-4f9b-a7b5-3cc727ebae7c

📥 Commits

Reviewing files that changed from the base of the PR and between 0852a5e and 5585e97.

📒 Files selected for processing (2)
  • app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
  • app/src/test/java/com/itsaky/androidide/activities/editor/RestorePluginTabsThreadTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
  • app/src/test/java/com/itsaky/androidide/activities/editor/RestorePluginTabsThreadTest.kt

📝 Walkthrough
  • Moved EditorHandlerActivity.restoreOpenedPluginTabs() work off the main thread during app startup by wrapping SharedPreferences access, JSON decoding, and cache clearing in coroutine background dispatchers.

  • Kept UI-facing tab selection/state updates within the launched coroutine flow, preserving behavior while reducing main-thread blocking and ANR risk.

  • Added a Robolectric regression test that verifies the cached-tabs preference is cleared from a background thread rather than the main test thread.

  • Risk: the change relies on coroutine dispatcher behavior and lifecycle timing during startup, so regressions could surface if the restore flow races with activity teardown or UI initialization.

  • Risk: review noted a possible collision with another in-flight PR, so merge conflicts or overlapping behavior changes should be checked carefully.

Walkthrough

restoreOpenedPluginTabs() now runs cached preference loading, JSON parsing, and cache clearing through coroutines instead of a single synchronous flow. A Robolectric regression test verifies the preference-clearing write occurs off the main thread.

Changes

Async plugin tab restore with threading regression test

Layer / File(s) Summary
Async restore implementation and threading regression test
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt, app/src/test/java/com/itsaky/androidide/activities/editor/RestorePluginTabsThreadTest.kt
restoreOpenedPluginTabs launches coroutine work to read PREF_KEY_OPEN_PLUGIN_TABS on Dispatchers.IO, parse JSON on Dispatchers.Default, restore tabs from the decoded IDs, and clear the cached preference key asynchronously. The Robolectric test seeds the cached JSON, preloads pluginTabIndices via reflection, invokes the private restore method, waits on an EventBus-backed latch, and asserts the preference-clearing write happens off the main/test thread.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • appdevforall/CodeOnTheGo#853: Introduced the EditorHandlerActivity.restoreOpenedPluginTabs() preference restore flow and the PREF_KEY_OPEN_PLUGIN_TABS persistence path that this PR changes.
  • appdevforall/CodeOnTheGo#952: Also touches EditorHandlerActivity lifecycle/threading safeguards around editor-side async work and tab handling.

Suggested reviewers

  • Daniel-ADFA
  • jomen-adfa

Poem

🐇 Hop to the IO lane, off the UI track,
JSON decodes, then the tabs hop back.
A latch in the burrow, a thread check at night,
Coroutines carry the cache out of sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving opened plugin tab decoding off the main thread.
Description check ✅ Passed The description matches the PR and explains the startup performance fix and regression test.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-4331-restoreplugintabs-json-main-thread

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

fryanpan and others added 3 commits June 19, 2026 09:58
Sentry APPDEVFORALL-1J. Move Gson decode in restoreOpenedPluginTabs to
Dispatchers.Default; keep UI updates on main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in thread

Failing-first repro that invokes the real private restoreOpenedPluginTabs()
under Robolectric and captures the thread on which the cached-tabs preference
write executes (via the EventBus PreferenceChangeEvent posted by
PreferenceManager.putString). UI tab-selection is skipped by pre-seeding the
private pluginTabIndices map.

Pre-fix: read + Gson decode + clearing write run synchronously on the main
thread -> write thread == main thread -> test FAILS.
Fixed: read/decode/write are wrapped in withContext(Dispatchers.IO/Default) ->
write runs on a background worker -> test PASSES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add KDoc to restoreOpenedPluginTabs() and the repro @test; remove a
leftover println("PROBE: ...") debug line in RestorePluginTabsThreadTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fryanpan fryanpan force-pushed the ADFA-4331-restoreplugintabs-json-main-thread branch from 9d4f64c to 99467d1 Compare June 19, 2026 16:58
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

Please be careful about possible collisions with PR #1426

@Daniel-ADFA Daniel-ADFA merged commit 97e5c06 into stage Jun 25, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-4331-restoreplugintabs-json-main-thread branch June 25, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants